Change f32::midpoint to upcast to f64 - #121062
Conversation
|
rustbot has assigned @Mark-Simulacrum. Use r? to explicitly pick a reviewer |
|
r? @scottmcm |
commented
Feb 14, 2024
|
No need for a codegen test for rust code in core unless you're checking to ensure that a particular optimization is triggering on it -- most commonly it's for testing that particular instantiations simplify. |
|
Just to try to (informally) prove it for myself:
Only question I'd have: I see tests in rust/library/core/tests/num/mod.rs Line 721 in a84bb95 a/2 + b/2 implementation is a single rounding, because halving doesn't round, assuming no underflow.)
|
commented
Feb 14, 2024
|
That would be a bad choice for CPUs that have a single-precision FPU and emulate double precision. I don't know if we support any such targets, but such CPUs exist (some 32bit ARMs) |
Seems like Not sure if we can do anything here about those targets, we don't have |
This looks correct
No, I don't think that they test large differences in magnitude. I'll add tests for that.
Hm, I could fall back to the original implementation if we're on an embedded target tier 2 target. (If this is bad on a tier 3 target, I can add support if someone asks for specific targets before this PR gets merged). Would work? That would cover |
|
Is there a reference that I can use to figure out which targets don't support double-precision floats? If not, I'll just use the original implementation on any target listed as Bare on the platform support page: https://doc.rust-lang.org/nightly/rustc/platform-support.html#tier-2-without-host-tools |
commented
Feb 15, 2024
No, not that I'm aware, this is the issue I alluded to in #121062 (comment), we don't have a One thing that could be done is to hack a cfg chain, like this, it should detect detect #[cfg(all(target_arch = "arm", target_pointer_width = "32", not(target_feature = "vfp2")))] |
commented
Feb 15, 2024
I don't think it's possible to get double rounding, as the precision of Subtraction instead of addition shouldn't be able to break through the about 6 bits of leeway. |
commented
Feb 16, 2024
commented
Feb 19, 2024
|
Have you benchmarked how this would affect an auto-vectorized loop of midpoint calculations? Upcasting to f64 means it can only have half the number of lanes which probably eats up some of the gains of the simpler algorithm. |
|
@the8472 I checked godbolt, and there is no noticeable SIMD parallelization in either implementation. They are both equivalent to the scalar implementation. On x86_64 they do use SIMD registers, but this isn't because they are using SIMD for parallelization, but because x86/_64 only implents IEEE floats in SIMD, not in scalar ASM (they use 80-bit floats there). So we emit SIMD code to ensure that we are IEEE compliant. I also did a simple benchmark to confirm my suspicions. You can find the code for the benchmark here: https://github.com/RustyYato/f32-perf-test The benchmarks were run with criterion, picking the best results across both runs for each implementation cargo bench
RUSTFLAGS='-C target-cpu=native' cargo benchNOTE: ps = pico seconds (1/1000 of 1 ns) For reference,
numbers in the range 0..f32::MAX (showing the worst cast of the original implementation)
Detailsmy hardware is: rustc version info I think this is pretty solid evidence that upcasting is better on current x86_64 hardware. But I don't have access to an ARM/powerpc/AARCH64 machine, so I can't verify other targets. edit: just realized that I used If there are any deficiencies in my benchmark, I'm happy to run them again after those are fixed |
commented
Feb 21, 2024
|
The benchmarks use |
|
Ok, after that change, (switching from I think stable performance is better than a 14% improvement. And if someone wanted to squeeze out that 10%, then they could copy out the original implementation.
I'm not sure how to do that, if you could leave some instructions on how to do that, then I could try that out. |
commented
Feb 21, 2024
Prefix the benchmark command with
I don't do enough float number crunching to have an intuition how common it is for bulk data to fit into one of the branches vs. being spread between them. And and I think it'll only matter if you do bulk data crunching. If you only do some float computation every now and then it doesn't matter either way. Anyway, now I think we have three questions:
This makes it complicated to assess whether it's worth it. |
I tried this and for most of the tests it didn't change performance, and for the ones it did only by ~1%. It seems the current implementation benefits from this more than the upcast implementation.
I don't think there would be a large spread in most cases, but I would prefer not to have performance pitfalls that only occur in special cases if they are easy to avoid.
In that case we shouldn't be considering that case, since either implementation will be fast enough.
I don't think any tier 1 targets do, for tier 2 or tier 3 targets, they can be added as special cases (or anyone using those targets can add those special cases if it matters). If the number of special cases gets large enough, maybe we can add a target specification for this instead.
If the difference isn't that large (no more than 30% faster/slower), I think the branchless algorithm is still fine.
I would expect any code which needs performance front and center, won't rely on autovectorization. And since the upcast version is only 14% slower (on my machine at least), I wouldn't expect this to be a deal breaker. Also this was a very artificial benchmark, I would expect the difference in more realistic code to be much closer. Esp. since the vector registers are used by all float ops on x86(_64). It would make it harder for autovectorization to trigger.
Yes, this was more involved that I expected 😄. No matter, I think we can sort this out. By chance, do you have a non-x86(_64) machine which you could test this out on? |
That's definitely not true. Writing architecture-specific SIMD code is a lot of work with all the features. Many people just beat the code into a shape that pleases LLVM and then rely on that.
Yeah, I can test on an aarch64 cloud machine. And maybe some other x86 hardware, seems to be tricky enough to be worth it. What's your CPU? |
commented
Feb 22, 2024
Agner Fog's x86 instruction tables lists DIVSS having a latency of 9-17 and DIVSD 9-32. So the worst case is almost twice as much. But the latency is data-dependent and I think a power-of-two divisor is cheap and so won't experience worst-case latencies. Icelake and various other *lake CPUs are listed as having an RThroughput (lower is better) of 3 for DIVSS vs. 4 for DIVSD. That'd be 33% slower in terms of throughput. So it could impact ILP which is a bit trickier to benchmark, you need the loop to do more work, with either some speculative execution or some independent data dependency chains. |
commented
Feb 22, 2024
13th Gen Intel(R) Core(TM) i7-13700K |
commented
Mar 5, 2024
The "hope auto vec" benches in your repo don't do that though. They still use |
commented
Mar 6, 2024
|
@the8472 my bad 🙃, I forgot to push my latest changes. They should be up now |
commented
Jun 2, 2024
|
These commits modify the If this was unintentional then you should revert the changes before this PR is merged. This PR changes how LLVM is built. Consider updating src/bootstrap/download-ci-llvm-stamp. Some changes occurred in coverage instrumentation. cc @Zalathar Some changes occurred to the core trait solver cc @rust-lang/initiative-trait-system-refactor Some changes occurred to MIR optimizations cc @rust-lang/wg-mir-opt Some changes occurred to the CTFE / Miri engine cc @rust-lang/miri Some changes occurred in match lowering cc @Nadrieril
|
This has been verified by kani as a correct optimization see: rust-lang#110840 (comment) The new implementation is branchless, and only differs in which NaN values are produced (if any are produced at all). Which is fine to change. Aside from NaN handling, this implementation produces bitwise identical results to the original implementation. The new implementation is gated on targets that have a fast 64-bit floating point implementation in hardware, and on WASM.
commented
Jun 2, 2024
|
Sorry about the noise above, my bad. I've squashed everything into a single commit, given how small the change is I don't think there's much point in multiple commits for this. |
commented
Jun 2, 2024
|
@bors r+ rollup |
commented
Jun 2, 2024
commented
Jun 3, 2024
|
bors what doing |
This has been verified by kani as a correct optimization
see: #110840 (comment)
The new implementation is branchless and only differs in which NaN values are produced (if any are produced at all), which is fine to change. Aside from NaN handling, this implementation produces bitwise identical results to the original implementation.
Question: do we need a codegen test for this? I didn't add one, since the original PR #92048 didn't have any codegen tests.